using System;
using System.Linq;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Event;
using NHibernate.Event.Default;
using NHibernate.Type;

namespace seanfoy.nascence {
    /// <summary>
    /// Associate a not-nascent bit with entities
    /// loaded by NHibernate, and use that to
    /// classify entities as transient or dirty.
    /// </summary>
    /// <remarks>
    /// <para>Deleted objects become transient
    /// again.</para>
    /// <para>I assume that keys are immutable.</para>
    /// </remarks>
    public class NascenceInterceptor : EmptyInterceptor {
        override public void OnDelete(Object entity, Object id, Object [] state, String [] propertyNames, IType [] types) {
            base.OnDelete(entity, id, state, propertyNames, types);
            DeregisterExtant(entity);
        }

        override public Boolean OnLoad(Object entity, Object id, Object [] state, String [] propertyNames, IType [] types) {
            Boolean result = base.OnLoad(entity, id, state, propertyNames, types);
            RegisterExtant(entity);
            return result;
        }

        override public Boolean OnSave(Object entity, Object id, Object [] state, String [] propertyNames, IType [] types) {
            Boolean result = base.OnSave(entity, id, state, propertyNames, types);
            RegisterExtant(entity);
            return result;
        }

        override public Boolean? IsTransient(Object entity) {
            if (NascentP(entity)) {
                return true;
            }
            else {
                return false;
            }
            return base.IsTransient(entity);
        }

        /// <summary>
        /// Do we believe this entity is represented in the
        /// database?
        /// </summary>
        public Boolean NascentP(Object entity) {
            return
                Object.ReferenceEquals(
                    FindExtant(
                        FindBucket(entity),
                        entity),
                    null);
        }

        private Dictionary<Int32, List<WeakReference>> extant = new Dictionary<Int32, List<WeakReference>>();
        public void RegisterExtant(Object entity) {
            var bucket =
                SetDefault(
                    extant,
                    entity.GetHashCode(),
                    () => new List<WeakReference>());
            var elt = FindExtant(bucket, entity);
            if (Object.ReferenceEquals(elt, null)) {
                bucket.Add(new WeakReference(entity, false));
            }
        }

        public void DeregisterExtant(Object entity) {
            var bucket = FindBucket(entity);
            if (bucket == null) return;
            var elt = FindExtant(bucket, entity);
            if (elt == null) return;
            bucket.Remove(elt);
        }

        private List<WeakReference> FindBucket(Object entity) {
            List<WeakReference> bucket = null;
            extant.TryGetValue(entity.GetHashCode(), out bucket);
            return bucket;
        }

        private WeakReference FindExtant(List<WeakReference> bucket, Object quarry) {
            if (Object.ReferenceEquals(bucket, null)) return null;
            WeakReference elt = null;
            // on average we'll observe half the elements
            // and if at least half that many are dead,
            // we'll compact.
            int threshold = bucket.Count / 4;
            foreach (WeakReference i in bucket) {
                if (i.IsAlive) {
                    if (Object.ReferenceEquals(i.Target, quarry)) {
                        elt = i;
                        break;
                    }
                }
                else {
                    if (threshold < 0) continue;
                    --threshold;
                }
            }
            if (threshold < 0) {
                extant[quarry.GetHashCode()] =
                    Enumerable.Where(
                        bucket,
                        i => i.IsAlive).ToList();
            }
            return elt;
        }

        public static U SetDefault<T, U>(Dictionary<T, U> dict, T key, Func<U> def) {
            if (!dict.ContainsKey(key)) {
                dict[key] = def();
            }
            return dict[key];
        }
    }
}